package jmine.tec.web.servlet.filter;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * {@link Filter} de Servlet que faz um synchronize na {@link HttpSession} antes de proceder a invocacao a outros filtros e/ou servlets
 * 
 * @author takeshi
 */
public class SessionSynchronizationFilter implements Filter {

    private static final String LOCK_IN_SESSION = "protected_lock";

    private static final String REDIRECT_URL = "/timeout";

    private static final long DEFAULT_WAIT_TIME_IN_SECONDS = 60;

    private String redirectUrl = REDIRECT_URL;

    private long waitTime = DEFAULT_WAIT_TIME_IN_SECONDS;

    /**
     * {@inheritDoc}
     */
    public void destroy() {
    }

    /**
     * {@inheritDoc}
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request instanceof HttpServletRequest && this.synchronizationRequired((HttpServletRequest) request, response)) {
            HttpSession session = ((HttpServletRequest) request).getSession(true);
            final Lock lock = this.getOrCreateLock(session);
            try {
                this.proceedFilterChain(request, response, chain, lock);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        } else {
            chain.doFilter(request, response);
        }
    }

    /**
     * Verifica se a sincronização é necessária
     * 
     * @param request request
     * @param response response
     * @return <code>true</code> se a sincronização é necessária, <code>false</code> caso contrário
     */
    protected boolean synchronizationRequired(HttpServletRequest request, ServletResponse response) {
        return !(request.getRequestURI().endsWith(this.getRedirectUrl()) && request.getParameterMap().isEmpty());
    }

    /**
     * Prossegue com a requisição se conseguir adquirir o lock, caso contrário redireciona para página de timeout
     * 
     * @param request request
     * @param response response
     * @param chain chain
     * @param lock lock
     * @throws InterruptedException InterruptedException
     * @throws IOException IOException
     * @throws ServletException ServletException
     */
    private void proceedFilterChain(ServletRequest request, ServletResponse response, FilterChain chain, final Lock lock)
            throws InterruptedException, IOException, ServletException {
        if (lock.tryLock(this.getWaitTime(), TimeUnit.SECONDS)) {
            try {
                chain.doFilter(request, response);
            } finally {
                lock.unlock();
            }
        } else {
            this.redirectTo(this.getRedirectUrl(), (HttpServletRequest) request, (HttpServletResponse) response);
        }
    }

    /**
     * @param url url a ser redirecionada
     * @param request request
     * @param response response
     * @throws ServletException ServletException
     * @throws IOException IOException
     */
    private void redirectTo(String url, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.sendRedirect(request.getContextPath() + url);
    }

    /**
     * Devolve o lock da sessão, se não existir, cria o {@link Lock} e adiciona na {@link HttpSession}
     * 
     * @param session session
     * @return the {@link Lock}
     */
    private Lock getOrCreateLock(HttpSession session) {
        Lock lock;
        synchronized (session) {
            lock = (Lock) session.getAttribute(LOCK_IN_SESSION);
            if (lock == null) {
                lock = new ReentrantLock();
                session.setAttribute(LOCK_IN_SESSION, lock);
            }
        }
        return lock;
    }

    /**
     * @return the redirectUrl
     */
    public String getRedirectUrl() {
        return redirectUrl;
    }

    /**
     * @param redirectUrl the redirectUrl to set
     */
    public void setRedirectUrl(String redirectUrl) {
        this.redirectUrl = redirectUrl;
    }

    /**
     * @return the waitTime
     */
    public long getWaitTime() {
        return waitTime;
    }

    /**
     * @param waitTime the waitTime to set
     */
    public void setWaitTime(long waitTime) {
        this.waitTime = waitTime;
    }

    /**
     * {@inheritDoc}
     */
    public void init(FilterConfig filterConfig) throws ServletException {
    }

}
